package jflowmap.model.ontology;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import jflowmap.model.graph.Node;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

public class Selection {
	private final Context context;
	private final View view;
	private SelectionDefinition currentSelection;
	
	private final Map<Dimension, Set<SelectionListener>> selectionListeners = Maps.newHashMap();
	private final Map<String, Set<Dimension>> selectedDimensions = Maps.newHashMap();
	private final List<SelectionUpdateListener> selectionUpdateListeners = Lists.newArrayList();
	
	Selection (View view) {
		this.view = view;
		this.context = view.getContext();
	}
	
	public Context getContext() {
		return context;
	}

	public Selection empty() {
		sendBeforeSelectionChangedEvent();		
		final Map<String, Set<Dimension>> tmp = Maps.newHashMap(selectedDimensions);
		selectedDimensions.clear();
		
		for (final Entry<String, Set<Dimension>> entry: tmp.entrySet()) {
			for (Dimension dim: entry.getValue()) {
				sendUnselectEvent(entry.getKey(), dim);
			}
		}
		sendAfterSelectionChangedEvent();
		return this;
	}

	public void addSelectionListener(Dimension dim, SelectionListener listener) {
		if (!selectionListeners.containsKey(dim)) {
			selectionListeners.put(dim, Sets.<SelectionListener>newHashSet(listener));
		} else {
			selectionListeners.get(dim).add(listener);
		}
	}
	
	public void removeSelectionListener(Dimension dim, SelectionListener listener) {
		if (selectionListeners.containsKey(dim)) {
			selectionListeners.get(dim).remove(listener);
		}
	}
	
	private void sendBeforeSelectionChangedEvent() {
		for (final SelectionUpdateListener listener: selectionUpdateListeners) {
			listener.beforeSelectionChanged();
		}
	}
	
	private void sendAfterSelectionChangedEvent() {
		for (final SelectionUpdateListener listener: selectionUpdateListeners) {
			listener.afterSelectionChanged();
		}
	}
	
	private void sendUnselectEvent(String category, Dimension dim) {
		Set<SelectionListener> listeners = selectionListeners.get(dim);
		if (listeners != null) {
			for (SelectionListener listener: listeners) {
				listener.unselect(category, dim);
			}
		}
	}
	
	private void sendSelectEvent(String category, Dimension dim) {
		Set<SelectionListener> listeners = selectionListeners.get(dim);
		if (listeners != null) {
			for (SelectionListener listener: listeners) {
				listener.select(category, dim);
			}
		}
	}
	
	public Selection unselect (String category, Set<Dimension> dimensions) {
		sendBeforeSelectionChangedEvent();
		if (!selectedDimensions.containsKey(category)) {
			return this;
		}
		
		final Set<Dimension> dimSet = selectedDimensions.get(category);
		dimensions = Sets.newHashSet(Sets.intersection(dimensions, dimSet));
		dimSet.removeAll(dimensions);
		
		for (Dimension unselected: dimensions) {
			sendUnselectEvent(category, unselected);
		}
		
		sendSelectedSelectionEvent(null);
		sendAfterSelectionChangedEvent();
		return this;
	}
	
	public Selection select (String category, Collection<Dimension> dimensions) {
		return select(category, Sets.newHashSet(dimensions));
	}
	
	public Selection select (String category, Set<Dimension> dimensions) {
		sendBeforeSelectionChangedEvent();
		if (!selectedDimensions.containsKey(category)) {
			selectedDimensions.put(category, Sets.<Dimension>newHashSet());
		}
		
		final Set<Dimension> dimSet = selectedDimensions.get(category);
		dimensions = Sets.newHashSet(Sets.difference(dimensions, dimSet));		
		dimSet.addAll(dimensions);
		
		
		for (Dimension d: dimensions) {
			sendSelectEvent(category, d);
		}
		
		checkSelectedSelection(dimSet, category);
		sendAfterSelectionChangedEvent();
		return this;
	}
	
	private void checkSelectedSelection(Set<Dimension> dimSet, String category) {
		final SelectionDefinition exact = context.getExactSelectionDefinition(dimSet);
		if (exact == null) {
			final Collection<SelectionDefinition> near = context.getNearSelectionDefinitions(dimSet);
			
			if (!near.contains(currentSelection)) {
				if (near.size() > 1) {
					final SelectionDefinition selectionDefinition = this.view.chooseSelection(near, dimSet);
					if (selectionDefinition != null) {
						this.select(category, selectionDefinition);
						sendSelectedSelectionEvent(selectionDefinition);
					}
				} else if (near.size() == 1) {
					final SelectionDefinition selectionDefinition = this.view.autocompleteSelection(near.iterator().next(), dimSet);
					if (selectionDefinition != null) {
						this.select(category, selectionDefinition);
						sendSelectedSelectionEvent(selectionDefinition);
					}
				} else {
					sendSelectedSelectionEvent(null);
				}
			} else {
				sendSelectedSelectionEvent(null);
			}
			
		} else {
			if (currentSelection != exact) {
				sendSelectedSelectionEvent(exact);
			}
		}
		
	}

	private void sendSelectedSelectionEvent(SelectionDefinition definition) {
		this.currentSelection = definition;
		for (final SelectionUpdateListener listener: selectionUpdateListeners) {
			listener.selectedSelection(definition);
		}
		
		if (definition != null) {
			System.out.println("Current selection: " + definition.getName());
		} else {
			System.out.println("Current selection: unnamed");
		}
	}

	public Selection selectOnly (String category, Set<Dimension> dimensions) {
		sendBeforeSelectionChangedEvent();
		if (!selectedDimensions.containsKey(category)) {
			return this.select(category, dimensions);
		}
		
		final Set<Dimension> dimSet = selectedDimensions.get(category);
		final Set<Dimension> unselected = Sets.newHashSet(Sets.difference(dimSet, dimensions)); 
		final Set<Dimension> selected = Sets.newHashSet(Sets.difference(dimensions, dimSet));
		dimSet.retainAll(dimensions);
		
		for (Dimension dim: unselected) {
			sendUnselectEvent(category, dim);
		}
		
		for (Dimension dim: selected) {
			sendSelectEvent(category, dim);
		}
		sendAfterSelectionChangedEvent();
		return this;
	}
	
	public Selection select (String category, DimensionCollection dimColl) {
		return this.select(category, dimColl.get());
	}
	
	public Selection select (String category, Dimension dimension) {
		return this.select(category, Sets.newHashSet(dimension));
	}
	
	public Selection select (String category, String dimensionName) {
		return this.select(category, context.getDimension(dimensionName));
	}
	
	public Selection unselect (String category, Dimension dimension) {
		return this.unselect(category, Sets.newHashSet(dimension));
	}
	
	public Selection unselect (String category, String dimensionName) {
		return this.unselect(category, context.getDimension(dimensionName));
	}
	
	public Selection unselect (String category, DimensionCollection dimColl) {
		return this.unselect(category, dimColl.get());
	}
	
	public Selection selectOnly (String category, Dimension dimension) {
		return this.selectOnly(category, Sets.newHashSet(dimension));
	}
	
	public Selection selectOnly (String category, String dimensionName) {
		return this.selectOnly(category, context.getDimension(dimensionName));
	}
	
	public Selection selectOnly (String category, DimensionCollection dimColl) {
		return this.selectOnly(category, dimColl.get());
	}
	
	public boolean isSelected (String category, Dimension dimension) {		
		if (selectedDimensions.containsKey(category)) {
			return selectedDimensions.get(category).contains(dimension);
		}
		
		return false;
	}
	
	public boolean isSelected (String category, String dimensionName) {
		return this.isSelected(category, context.getDimension(dimensionName));
	}
	
	public View getView() {
		return view;
	}

	public Selection select(Set<Node> nodes, String category) {
		final Set<Dimension> dims = Sets.newHashSet();
		for (Node node: nodes) {
			dims.add(node.getDimension());
		}
		return this.select(category, dims);		
	}

	public Selection toggle(String category, Dimension dimension) {
		if (selectedDimensions.containsKey(category)) {
			if (selectedDimensions.get(category).contains(dimension)) {
				return this.unselect(category, dimension);
			}
		}
		
		return this.select(category, dimension);
	}

	public Selection empty(String category) {
		if (selectedDimensions.containsKey(category)) {
			this.unselect(category, selectedDimensions.get(category));
		}
		
		return this;
	}

	public Set<Dimension> get(String string) {
		return Collections.unmodifiableSet(selectedDimensions.get(string));
	}

	public void addSelectionUpdateListener(SelectionUpdateListener listener) {
		selectionUpdateListeners.add(listener);
	}

	public void removeSelectionUpdateListener(SelectionUpdateListener listener) {
		selectionUpdateListeners.remove(listener);
	}
	
	public Selection select (String category, final SelectionDefinition selectionDefinition) {
		this.empty(category);
		this.select(category, selectionDefinition.getDimensions());
		return this;
	}
}
